package com.alex.maze;

import com.alex.maze.graphics.Screen;
import java.awt.Canvas;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import javax.swing.JFrame;

public class Main extends Canvas implements Runnable
{
    /*
        Pravite klasu koja je serijalizovana tj. implementira serijalizovani 
        interfejs onda uvek trebate deklarisati:
        private static final long serialVersionUID = 1L;
        Ako ne delarisete serialVersionUID, sistem ima da napravi svoju UID. Pa
        ako promenite bilo sta kasnije, UID ima da se promeni i onda stari 
        podaci nece biti vise dostupni. Ovo se moze resiti tako sto manuelno 
        izracunate hash vrednost koju je sistem koristio za staru klasu i onda
        je ubacite rucno. Taj hash kod moze ovako da izgleda:
        private static final long serialVersionUID = 3487495895819393L;
        Ovo nije veliki problem ali izgleda ruzno, mozete te ga lose napisati i 
        ne kompresuje se dobro. Ne dobijate nista tako sto ovu deklaraciju ne
        napisete na samom pocetku programa. Zbog toga vrednost od 1 je najcistija
        i najlepsa. Prosto i jednostavno.
    */
    private static final long serialVersionUID = 1L;
    
    /*  Koristim LDTV standar poznatiji jos kao i 240p u 16:9 formatu.  */
    private static final int SIRINA =  360; // Povecao sam rez sad ke 360p
    private static final int VISINA = 180; // Mozda povecam cak na 480p
    
    /* 
        Skala pvecava pojedinacan piksel po cetri puta. Tako da rezolucija 
        izgleda kao da je SIRINA = 1708 i VISINA = 960. Ako imate probem da 
        je prozor previse velik za vas ekran onda smanjite ovu vrednost na 3, 2 
        ili 1.
    */
    private static final int SKALA = 3;
    
    /*  Ime programa koje ce biti isipano na tajtl baru. */ 
    private static final String IME = "_maze_";
    
    /*  Bulean koji pomaze oko rada sa nitima. */
    private boolean radi = false;
    
    /*  Deklarisemo objektat nit.  */
    private Thread nit;
    
    private Game igra;
    private Screen screen;
    private BufferedImage slika;
    private int[] pikseli;
    
    private InputHandler inputHandler;
   
    /* 
        synchronized znaci da Java kaze JVM samo jedna nit moze ovde (ne vise, ne manje).
        Metoda koja prvo proverava da li je bul radi istinit ili ne.
        Nakon toga postavlja vrednost bula radi na istinit.
        Pa nakon toga inicijalizuje i pokrece nit.
    */
    public synchronized void start()
    {
        if(radi)
        {
            System.err.println("Bul radi je vec istinit!");
            System.exit(0);
        }
        
        radi = true;
        
        nit = new Thread(this);
        nit.start();
    }
    
    /* 
        synchronized znaci da Java kaze JVM samo jedna nit moze ovde (ne vise, ne manje).
        Metoda koja prvo proverava da li je bul radi istinit ili ne.
        Nakon toga postavlja vrednost bula radi na neistinit.
        Pa nakon toga spaja niti.
    */
    public synchronized void stop()
    {
        if (!radi)
        {
            System.err.println("Bul radi je vec neistinit!");
            System.exit(0);
        }
        
        radi = false;
        
        try
        {
            nit.join();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    /*
        Overajdovana metoda
        koja treba da se deklarise, zbog toga sto main metoda implementira
        Runnable.
    */
    @Override
    public void run()
    {
        // Broj frejmova u sekundi
        int frejmovi = 0;
        
        double neprocesiranoSekunde = 0;
        /*  Pre while petlje... */
        long prethodnoVreme = System.nanoTime();
        /*  Brzina po kojoj azuriranje vrsi */
        double sekundePoAzuriranju = 1 / 60.0;
        /*
            Broj azuriranja  kao sto ime kaze. U principu na kada brojaAzuriranja
            bude 60 pa onda 120 pa onda 180 onda tada prikazemo broj frejmova i
            te iste frejmove vratimo na 0.
        */
        int brojacAzuriranja = 0;
        
        requestFocus(); // Ovo sam trebao da uradi jos odavno!
        
        while(radi)
        {
            /*  U while petlji... */
            long sadasnjeVreme = System.nanoTime();
            /*  Vreme koje je proteklo izmedju sadasnjeg i prethodnog vremena itd... */
            long protekloVreme = sadasnjeVreme - prethodnoVreme;
            /*  Mora da se postavi da bude sadasnje vreme, bilo sta drugo i ne radi kako treba! */
            prethodnoVreme = sadasnjeVreme;
            /* Ako se nesto zlo desi ovo ce da ga sredi :) */
            if (protekloVreme < 0)
            {
                protekloVreme = 0;
            }
            if (protekloVreme > 1000000000)
            {
                protekloVreme = 1000000000;
            }
            
            neprocesiranoSekunde += protekloVreme / 1000000000.0;
            
            /*  Objasni! */
            boolean azurirano = false;
            while(neprocesiranoSekunde > sekundePoAzuriranju)
            {
                /*  !!!NOTE!!! - Uvek prvo treba da ide azuriranje pa tek onda render */
                azuriranje();
                neprocesiranoSekunde -= sekundePoAzuriranju;
                azurirano = true;
                
                /*  Objasnio sam gore. */
                brojacAzuriranja++;
                if (brojacAzuriranja % 60 == 0)
                {
                    System.out.println(frejmovi +"fps");
                    frejmovi = 0; 
                }
            }
            
            /* Ako oceste da iskljucite frame limit (60fps) onda komentarisite if petlju!*/
            //if (azurirano)
            //{ 
                render();
                frejmovi++;
            //}
        }
    }
    
    /* Metoda koja je odgovorna za "komputacionalne" stvari :) */
    public void azuriranje()
    {
        if (hasFocus())
        {
            igra.azuriraj(inputHandler.keys); // Haxorzzzz!!! Mali hack mrzi me da radim...
        }
    }
    
    /*  Metoda koja je odgovorna za iscrtavanje grafike. */
    public void render()
    {
        /*  Za vise pogledajte oraklovu dokumentaciju za BufferStrategy */
        BufferStrategy bs = getBufferStrategy();
        if (bs == null)
        {
            /*
                Za createBufferStrategy koritimo brojeve 2 ili 3.
                2 je za double buffering a 3 je za triple buffering.
            */
            createBufferStrategy(3);
            return;
        }
        
        /*  Komentarisi pa vidi :P */
        screen.render(igra, hasFocus());

        for (int i = 0; i < SIRINA * VISINA; i++)
        {
            /*  Kopiramo vrednost screen.pikseli u pikseli! */
            pikseli[i] = screen.pikseli[i];
        }
        Graphics g = bs.getDrawGraphics();
        g.drawImage(slika, 0, 0, SIRINA * SKALA, VISINA * SKALA, null);
        /*  Uvek moramo da ispraznimo graficki objekat! */
        g.dispose();
        /* 
            Prikazuje sliku, piksele, grafiku i sav taj dzez!
            Ako zelite da vidite kako igra izgleda bez bs.show();
            onda ga komentarisete i pokrenete progam, voilÃ !
        */
        bs.show();
    }
    
    /*  Konstruktor */
    public Main()
    {
        /*  Postavljamo dimenzije prozora, ovo mozemo i drugacije da uradimo ali je preferiram ovako */
        Dimension velicina = new Dimension(SIRINA * SKALA, VISINA * SKALA);
        /*
            Ovaj korak se ne treba preskociti! 
            U principu potrebno je samo napisati ovo setPreferredSize();
            Pa onda ova druga dva nisu potrebna ali ja sam paranojik O_o
        */
        setPreferredSize(velicina); setMinimumSize(velicina); setMinimumSize(velicina);
        
        /*  Razne inicijalizacije */
        igra = new Game();
        screen = new Screen(SIRINA, VISINA);
        
        slika = new BufferedImage(SIRINA, VISINA, BufferedImage.TYPE_INT_RGB);
        /*int array = ((Kastujemo) objekat koji se kastuje ---> Da se lakse razume*/
        pikseli = ((DataBufferInt) slika.getRaster().getDataBuffer()).getData();
        
        inputHandler = new InputHandler();
        
        
        addKeyListener(inputHandler);
        addFocusListener(inputHandler);
        addMouseListener(inputHandler);
        addMouseMotionListener(inputHandler);
    }
    
    /*  Main metoda ili ulazna metoda. */
    public static void main(String[] args)
    {
        Main igraKomponenta = new Main();
        JFrame frejm = new JFrame(); 
        
        /* 
            Ovo je bug fix. 
            Postoji greska ako se setResizable stavio posle add-a i pack-a metode.
            Na donjoj i desnoj strani dolazi do treperenja u razmeri od nekih
            10 piksela. Postoje nekoliko nacina da se ovo resi ali ovo je naj
            elegantniji.
        */
        frejm.setResizable(false); // Da li mozemo promenimo velicinu prozora.
        frejm.add(igraKomponenta); // Dodajemo objekat igru.
        frejm.pack(); // Prozor ce naslediti vrednost objekta velicine.
        frejm.setTitle(IME); // Postavljamo ime prozora.
        frejm.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Da bi X dugme moglo lepo da radi.
        frejm.setLocationRelativeTo(null); // Centrira prozor na sredinu ekrana. 
        frejm.setVisible(true); // Postavljamo vidljivost prozora.
        
        igraKomponenta.start(); // Pokrecemo start metodu.
    }
}
